{
  "nbformat": 4,
  "nbformat_minor": 0,
  "metadata": {
    "colab": {
      "name": "PPG_verification_system.ipynb",
      "provenance": [],
      "collapsed_sections": []
    },
    "kernelspec": {
      "name": "python3",
      "display_name": "Python 3"
    },
    "accelerator": "GPU"
  },
  "cells": [
    {
      "cell_type": "code",
      "metadata": {
        "id": "SpiLoLO6do6w",
        "outputId": "f2965de2-4959-4e00-e7b9-41a9108f72cb",
        "colab": {
          "base_uri": "https://localhost:8080/"
        }
      },
      "source": [
        "%tensorflow_version 1.x\n",
        "import tensorflow as tf \n",
        "print(tf.__version__)\n"
      ],
      "execution_count": 1,
      "outputs": [
        {
          "output_type": "stream",
          "text": [
            "TensorFlow 1.x selected.\n",
            "1.15.2\n"
          ],
          "name": "stdout"
        }
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "2G0zb-pWeAS3"
      },
      "source": [
        "**Initialize**"
      ]
    },
    {
      "cell_type": "code",
      "metadata": {
        "id": "LBeEwXXUdo9r"
      },
      "source": [
        "import tensorflow as tf\n",
        "import numpy as np\n",
        "import matplotlib.pyplot as plt\n",
        "import scipy.io\n",
        "from scipy import stats\n",
        "from sklearn.preprocessing import OneHotEncoder\n",
        "from sklearn.utils import resample\n",
        "from sklearn.utils import class_weight\n",
        "from sklearn.model_selection import KFold\n",
        "import time\n",
        "import os\n",
        "\n",
        "read_file=scipy.io.loadmat('Subset_Biosec3.mat') # Load data\n"
      ],
      "execution_count": 2,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "jb8nkiXWpBdb"
      },
      "source": [
        "**Set Target ID, Model type**"
      ]
    },
    {
      "cell_type": "code",
      "metadata": {
        "id": "4EBc9hS3fg0l",
        "outputId": "bcf6d2fe-dfd9-44c7-d83f-c7b1b40d30bb",
        "colab": {
          "base_uri": "https://localhost:8080/"
        }
      },
      "source": [
        "ID = 1 # Target ID\n",
        "Model_type = 0 # Model type. 0: WS model / 1: ND model\n",
        "\n",
        "subject = np.arange(1,21).tolist()\n",
        "print('ID',ID)"
      ],
      "execution_count": 3,
      "outputs": [
        {
          "output_type": "stream",
          "text": [
            "ID 1\n"
          ],
          "name": "stdout"
        }
      ]
    },
    {
      "cell_type": "code",
      "metadata": {
        "id": "z4AKAmbkdpA5"
      },
      "source": [
        "X_train = np.zeros((2520,150,4)) \n",
        "Y_train = np.zeros((2520,1)) \n",
        "X_train[0:2400,:,:] = np.array(read_file['train_data']) # Assign original train data\n",
        "X_train[2400:,:,:] = np.array(read_file['PBGAN_DC_syn'])[subject.index(ID)*120:(subject.index(ID)+1)*120,:,:] # Assign synthetic data (from PBGAN-DC) for only target\n",
        "Y_train[0:2400,:] = np.array(read_file['train_label']) # Assign original train label\n",
        "Y_train[2400:] = ID*np.ones((120,1)) # Assign synthetic data's label\n",
        "\n",
        "X_test = np.array(read_file['test_data']) # Assign original test data\n",
        "Y_test = np.array(read_file['test_label']) # Assign original test label\n",
        "\n",
        "# Number of class\n",
        "n_classes= 20\n",
        "# Number of features\n",
        "n_features=X_train.shape[1]\n"
      ],
      "execution_count": 4,
      "outputs": []
    },
    {
      "cell_type": "code",
      "metadata": {
        "id": "oL8hrgRldpDu",
        "outputId": "01289ec7-517b-4197-b3cc-294e10ed625a",
        "colab": {
          "base_uri": "https://localhost:8080/"
        }
      },
      "source": [
        "Y_train[np.where(Y_train!=ID)] = 0 # Give '1' to Target ID while others are '0'\n",
        "Y_test[np.where(Y_test!=ID)] = 0 # Give '1' to Target ID while others are '0'\n",
        "\n",
        "print(np.unique(Y_train, return_counts=True)) # Check the distribution of train label\n",
        "print(np.unique(Y_test, return_counts=True)) # Check the distribution of test label"
      ],
      "execution_count": 5,
      "outputs": [
        {
          "output_type": "stream",
          "text": [
            "(array([0., 1.]), array([2280,  240]))\n",
            "(array([0, 1], dtype=uint8), array([456,  24]))\n"
          ],
          "name": "stdout"
        }
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "wdf_91zjgMwu"
      },
      "source": [
        "**Onehot Encoding and Data Shuffle**"
      ]
    },
    {
      "cell_type": "code",
      "metadata": {
        "id": "lRufy35EdpG5"
      },
      "source": [
        "enc = OneHotEncoder()\n",
        "\n",
        "randIndx = np.arange(X_train.shape[0])\n",
        "np.random.shuffle(randIndx)\n",
        "\n",
        "randIndx2 = np.arange(X_test.shape[0])\n",
        "np.random.shuffle(randIndx2)\n",
        "\n",
        "X_train = X_train[randIndx,:,:] # Random shuffle the train data\n",
        "X_test = X_test[randIndx2,:,:] # Random shuffle the test data\n",
        " \n",
        "Y_train =enc.fit_transform(Y_train[randIndx].reshape(-1,1)).toarray() # Random shuffle with one-hot encoding (train label)\n",
        "Y_test =enc.fit_transform(Y_test[randIndx2].reshape(-1,1)).toarray() # Random shuffle with one-hot encoding (test label)\n",
        "\n",
        "train_data, train_target = X_train, Y_train # Final assign train data/label\n",
        "test_data, test_target = X_test, Y_test # Final assign test data/label"
      ],
      "execution_count": 6,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "3NYlKES4ldwK"
      },
      "source": [
        "**WS Model Structure**\n"
      ]
    },
    {
      "cell_type": "code",
      "metadata": {
        "id": "iZsGJ4DddpTD"
      },
      "source": [
        "class WS_Model:\n",
        "\n",
        "    def __init__(self, sess, name):\n",
        "\n",
        "        self.sess = sess\n",
        "        self.name = name\n",
        "        self._build_net()\n",
        "\n",
        "    def _build_net(self):\n",
        "\n",
        "        with tf.variable_scope(self.name):\n",
        "\n",
        "            self.training = tf.placeholder(tf.bool) # Turn on/off dropout\n",
        "            self.X = tf.placeholder(tf.float32, [None, 150, 1]) # Input data\n",
        "            self.Y = tf.placeholder(tf.float32, [None, 2]) # Input label\n",
        "            self.learning_rate = tf.placeholder(tf.float32) # Learning rate\n",
        "            self.class_weights = tf.placeholder(tf.float32) # Weight for weighted cross entropy\n",
        "\n",
        "            # Convolutional Layer #1\n",
        "            W1 = tf.Variable(tf.random_normal([60, 1, 50], stddev=0.01)) # Initialize parameters from normal distribution \n",
        "            conv1 = tf.nn.conv1d(self.X, W1, stride=1, padding='VALID') # Convolution\n",
        "            relu1 = tf.nn.relu(conv1) # ReLu\n",
        "            dropout1 = tf.layers.dropout(inputs=relu1, rate=0.5, training=self.training) # Dropout (50%)\n",
        "            \n",
        "            # Convolutional Layer #2\n",
        "            W2 = tf.Variable(tf.random_normal([70, 50, 70], stddev=0.01)) # Initialize parameters from normal distribution \n",
        "            conv2 = tf.nn.conv1d(dropout1, W2, stride=1, padding='VALID') # Convolution\n",
        "            relu2 = tf.nn.relu(conv2) # ReLu                                   \n",
        "            dropout2 = tf.layers.dropout(inputs=relu2, rate=0.5, training=self.training) # Dropout (50%)            \n",
        "        \n",
        "            # FC Layer\n",
        "            FC = tf.reshape(dropout2, [-1, 22*70])         \n",
        "            FC_weight = tf.get_variable(\"FC_weight\", shape=[22*70, 2], initializer=tf.contrib.layers.xavier_initializer())\n",
        "            logits = tf.matmul(FC, FC_weight)             \n",
        "        \n",
        "        self.cost = tf.reduce_mean(tf.nn.weighted_cross_entropy_with_logits(self.Y, logits, self.class_weights)) # Weighted cross entropy with logits and labels\n",
        "        reg_losses = tf.nn.l2_loss(W1) + tf.nn.l2_loss(W2) + tf.nn.l2_loss(FC_weight) # L2 regularization\n",
        "        self.cost = self.cost + 0.05 * reg_losses # Final cost for optimization\n",
        "        self.optimizer = tf.train.AdamOptimizer(learning_rate=self.learning_rate).minimize(self.cost) # ADAM optimization\n",
        "\t\t\n",
        "        self.sigmoid_out = tf.nn.sigmoid(logits) \n",
        "        self.predict = tf.cast(self.sigmoid_out[:,1], tf.float32) # Output from model\n",
        "                    \n",
        "\n",
        "    def get_predict(self, x_test, training = False):          \n",
        "\n",
        "        return self.sess.run(self.predict, feed_dict={self.X: x_test, self.training: training})\n",
        "    \n",
        "\n",
        "    def get_cost(self, x_valid, y_valid, weights, training = False):\n",
        "        \n",
        "        return self.sess.run(self.cost, feed_dict={self.X: x_valid, \n",
        "                                                   self.Y: y_valid, self.class_weights: weights, self.training: training})\n",
        "\n",
        "    def train(self, x_data, y_data, weights, learning, training = True):\n",
        "\n",
        "        return self.sess.run(self.optimizer, feed_dict={\n",
        "             self.X: x_data, self.Y: y_data, self.class_weights: weights, self.learning_rate: learning, self.training: training})        \n"
      ],
      "execution_count": 7,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "aLemHr5BrdW7"
      },
      "source": [
        "**ND Model Structure**\n"
      ]
    },
    {
      "cell_type": "code",
      "metadata": {
        "id": "EE-ERhHnrcIe"
      },
      "source": [
        "class ND_Model:\n",
        "\n",
        "    def __init__(self, sess, name):\n",
        "\n",
        "        self.sess = sess\n",
        "        self.name = name\n",
        "        self._build_net()\n",
        "\n",
        "    def _build_net(self):\n",
        "\n",
        "        with tf.variable_scope(self.name):\n",
        "\n",
        "            self.training = tf.placeholder(tf.bool) # Turn on/off dropout\n",
        "            self.X = tf.placeholder(tf.float32, [None, 150, 1]) # Input data\n",
        "            self.Y = tf.placeholder(tf.float32, [None, 2]) # Input label\n",
        "            self.learning_rate = tf.placeholder(tf.float32) # Learning rate\n",
        "            self.class_weights = tf.placeholder(tf.float32) # Weight for weighted cross entropy\n",
        "\n",
        "            W = tf.Variable(tf.random_normal([7, 1, 1], stddev=0.01)) # Initialize parameters from normal distribution \n",
        "            conv = tf.nn.conv1d(self.X, W, stride=1, padding='SAME') # Convolution\n",
        "\t\t\t\n",
        "            # Residual Block #1 \n",
        "            relu1_1 = tf.nn.relu(conv) # ReLu\n",
        "            dropout1_1 = tf.layers.dropout(inputs=relu1_1, rate=0.5, training=self.training) # Dropout (50%)\n",
        "      \n",
        "            W1_1 = tf.Variable(tf.random_normal([7, 1, 16], stddev=0.01)) # Initialize parameters from normal distribution               \n",
        "            conv1_1 = tf.nn.conv1d(dropout1_1, W1_1, stride=2, padding='SAME') # Convolution\n",
        "            relu1_2 = tf.nn.relu(conv1_1) # ReLu\n",
        "            dropout1_2 = tf.layers.dropout(inputs=relu1_2, rate=0.5, training=self.training) # Dropout (50%)\n",
        "\n",
        "            W1_2 = tf.Variable(tf.random_normal([7, 16, 16], stddev=0.01)) # Initialize parameters from normal distribution               \n",
        "            conv1_2 = tf.nn.conv1d(dropout1_2, W1_2, stride=1, padding='SAME') # Convolution\n",
        "            \n",
        "            dim_W_1 = tf.Variable(tf.random_normal([1, 1, 16], stddev=0.01)) # Initialize parameters from normal distribution    \n",
        "            dim_1 = tf.nn.conv1d(conv, dim_W_1, stride=1, padding='SAME') # Convolution\n",
        "            avg_pool_1 = tf.nn.avg_pool1d(dim_1,ksize=2,strides=2,padding='SAME')\n",
        "\n",
        "            res_1 = avg_pool_1 + conv1_2 \n",
        "\n",
        "            # Residual Block #2 \n",
        "            relu2_1 = tf.nn.relu(res_1) # ReLu\n",
        "            dropout2_1 = tf.layers.dropout(inputs=relu2_1, rate=0.5, training=self.training) # Dropout (50%)\n",
        "\n",
        "            W2_1 = tf.Variable(tf.random_normal([7, 16, 32], stddev=0.01)) # Initialize parameters from normal distribution \n",
        "            conv2_1 = tf.nn.conv1d(dropout2_1, W2_1, stride=2, padding='SAME') # Convolution\n",
        "            relu2_2 = tf.nn.relu(conv2_1) # ReLu\n",
        "            dropout2_2 = tf.layers.dropout(inputs=relu2_2, rate=0.5, training=self.training) # Dropout (50%)\n",
        "\n",
        "            W2_2 = tf.Variable(tf.random_normal([7, 32, 32], stddev=0.01)) # Initialize parameters from normal distribution               \n",
        "            conv2_2 = tf.nn.conv1d(dropout2_2, W2_2, stride=1, padding='SAME') # Convolution\n",
        "\n",
        "            dim_W_2 = tf.Variable(tf.random_normal([1, 16, 32], stddev=0.01)) # Initialize parameters from normal distribution    \n",
        "            dim_2 = tf.nn.conv1d(res_1, dim_W_2, stride=1, padding='SAME') # Convolution\n",
        "            avg_pool_2 = tf.nn.avg_pool1d(dim_2,ksize=2,strides=2,padding='SAME')\n",
        "\n",
        "            res_2 = avg_pool_2 + conv2_2 \n",
        "\n",
        "             \n",
        "            # Residual Block #3 \n",
        "            relu3_1 = tf.nn.relu(res_2) # ReLu\n",
        "            dropout3_1 = tf.layers.dropout(inputs=relu3_1, rate=0.5, training=self.training) # Dropout (50%)\n",
        "\n",
        "            W3_1 = tf.Variable(tf.random_normal([7, 32, 64], stddev=0.01)) # Initialize parameters from normal distribution \n",
        "            conv3_1 = tf.nn.conv1d(dropout3_1, W3_1, stride=2, padding='SAME') # Convolution\n",
        "            relu3_2 = tf.nn.relu(conv3_1) # ReLu\n",
        "            dropout3_2 = tf.layers.dropout(inputs=relu3_2, rate=0.5, training=self.training) # Dropout (50%)\n",
        "\n",
        "            W3_2 = tf.Variable(tf.random_normal([7, 64, 64], stddev=0.01)) # Initialize parameters from normal distribution               \n",
        "            conv3_2 = tf.nn.conv1d(dropout3_2, W3_2, stride=1, padding='SAME') # Convolution\n",
        "\n",
        "            dim_W_3 = tf.Variable(tf.random_normal([1, 32, 64], stddev=0.01)) # Initialize parameters from normal distribution    \n",
        "            dim_3 = tf.nn.conv1d(res_2, dim_W_3, stride=1, padding='SAME') # Convolution\n",
        "            avg_pool_3 = tf.nn.avg_pool1d(dim_3,ksize=2,strides=2,padding='SAME')\n",
        "\n",
        "            res_3 = avg_pool_3 + conv3_2 \n",
        "\n",
        "\n",
        "            # Residual Block #4 \n",
        "            relu4_1 = tf.nn.relu(res_3) # ReLu\n",
        "            dropout4_1 = tf.layers.dropout(inputs=relu4_1, rate=0.5, training=self.training) # Dropout (50%)\n",
        "\n",
        "            W4_1 = tf.Variable(tf.random_normal([7, 64, 128], stddev=0.01)) # Initialize parameters from normal distribution \n",
        "            conv4_1 = tf.nn.conv1d(dropout4_1, W4_1, stride=2, padding='SAME') # Convolution\n",
        "            relu4_2 = tf.nn.relu(conv4_1) # ReLu\n",
        "            dropout4_2 = tf.layers.dropout(inputs=relu4_2, rate=0.5, training=self.training) # Dropout (50%)\n",
        "\n",
        "            W4_2 = tf.Variable(tf.random_normal([7, 128, 128], stddev=0.01)) # Initialize parameters from normal distribution               \n",
        "            conv4_2 = tf.nn.conv1d(dropout4_2, W4_2, stride=1, padding='SAME') # Convolution\n",
        "\n",
        "            dim_W_4 = tf.Variable(tf.random_normal([1, 64, 128], stddev=0.01)) # Initialize parameters from normal distribution    \n",
        "            dim_4 = tf.nn.conv1d(res_3, dim_W_4, stride=1, padding='SAME') # Convolution\n",
        "            avg_pool_4 = tf.nn.avg_pool1d(dim_4,ksize=2,strides=2,padding='SAME')\n",
        "\n",
        "            res_4 = avg_pool_4 + conv4_2 \n",
        "\n",
        "            # Residual Block #5 \n",
        "            relu5_1 = tf.nn.relu(res_4) # ReLu\n",
        "            dropout5_1 = tf.layers.dropout(inputs=relu5_1, rate=0.5, training=self.training) # Dropout (50%)\n",
        "\n",
        "            W5_1 = tf.Variable(tf.random_normal([7, 128, 256], stddev=0.01)) # Initialize parameters from normal distribution \n",
        "            conv5_1 = tf.nn.conv1d(dropout5_1, W5_1, stride=2, padding='SAME') # Convolution\n",
        "            relu5_2 = tf.nn.relu(conv5_1) # ReLu\n",
        "            dropout5_2 = tf.layers.dropout(inputs=relu5_2, rate=0.5, training=self.training) # Dropout (50%)\n",
        "\n",
        "            W5_2 = tf.Variable(tf.random_normal([7, 256, 256], stddev=0.01)) # Initialize parameters from normal distribution               \n",
        "            conv5_2 = tf.nn.conv1d(dropout5_2, W5_2, stride=1, padding='SAME') # Convolution\n",
        "\n",
        "            dim_W_5 = tf.Variable(tf.random_normal([1, 128, 256], stddev=0.01)) # Initialize parameters from normal distribution    \n",
        "            dim_5 = tf.nn.conv1d(res_4, dim_W_5, stride=1, padding='SAME') # Convolution\n",
        "            avg_pool_5 = tf.nn.avg_pool1d(dim_5,ksize=2,strides=2,padding='SAME')\n",
        "\n",
        "            res_5 = avg_pool_5 + conv5_2         \n",
        "\n",
        "            # FC Layer\n",
        "            FC_relu = tf.nn.relu(res_5) # ReLu\n",
        "\n",
        "            FC = tf.reshape(FC_relu, [-1, 5*256])         \n",
        "            FC_weight = tf.get_variable(\"FC_weight\", shape=[5*256, 2], initializer=tf.contrib.layers.xavier_initializer())\n",
        "            logits = tf.matmul(FC, FC_weight)             \n",
        "        \n",
        "        self.cost = tf.reduce_mean(tf.nn.weighted_cross_entropy_with_logits(self.Y, logits, self.class_weights)) # Weighted cross entropy with logits and labels\n",
        "        # L2 regularization\n",
        "        reg_losses = tf.nn.l2_loss(W) + tf.nn.l2_loss(W1_1) + tf.nn.l2_loss(W1_2) + tf.nn.l2_loss(W2_1) + \\\n",
        "         tf.nn.l2_loss(W2_2) + tf.nn.l2_loss(W3_1) + tf.nn.l2_loss(W3_2) + tf.nn.l2_loss(W4_1) + tf.nn.l2_loss(W4_2) + tf.nn.l2_loss(W5_1) + tf.nn.l2_loss(W5_2) + tf.nn.l2_loss(FC_weight)\n",
        "\n",
        "        self.cost = self.cost + 0.05 * reg_losses # Final cost for optimization\n",
        "        self.optimizer = tf.train.AdamOptimizer(learning_rate=self.learning_rate).minimize(self.cost) # ADAM optimization\n",
        "\t\t\n",
        "        self.sigmoid_out = tf.nn.sigmoid(logits) \n",
        "        self.predict = tf.cast(self.sigmoid_out[:,1], tf.float32) # Output from model\n",
        "\n",
        "    def get_predict(self, x_test, training = False):          \n",
        "\n",
        "        return self.sess.run(self.predict, feed_dict={self.X: x_test, self.training: training})\n",
        "    \n",
        "\n",
        "    def get_cost(self, x_valid, y_valid, weights, training = False):\n",
        "        \n",
        "        return self.sess.run(self.cost, feed_dict={self.X: x_valid, \n",
        "                                                   self.Y: y_valid, self.class_weights: weights, self.training: training})\n",
        "\n",
        "    def train(self, x_data, y_data, weights, learning, training = True):\n",
        "\n",
        "        return self.sess.run(self.optimizer, feed_dict={\n",
        "             self.X: x_data, self.Y: y_data, self.class_weights: weights, self.learning_rate: learning, self.training: training})        \n"
      ],
      "execution_count": 8,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "mx1w0DQjpNEg"
      },
      "source": [
        "**Train the Model**"
      ]
    },
    {
      "cell_type": "code",
      "metadata": {
        "id": "23mGQEKbdpWH",
        "outputId": "0f1f4720-f8fd-4725-94b0-8b9e78e07630",
        "colab": {
          "base_uri": "https://localhost:8080/"
        }
      },
      "source": [
        "# Hyperparameters\n",
        "learning_rate = 0.0001\n",
        "training_epochs = 60 # Number of epoch\n",
        "batch_size = 252 # Mini-batch size\n",
        "num_cv = 10 # Number of fold in cross validation \n",
        "bag_size = 2268  # Size of train set in cross validation\n",
        "\n",
        "valid_cost = np.zeros((training_epochs,4))\n",
        "\n",
        "\n",
        "sess = tf.Session()\n",
        "models = []\n",
        "num_models = 4 # Generate 4 CNNs for each input (i.e. DTW,TP,FP,Cubic)\n",
        "\n",
        "if Model_type == 0:\n",
        "\n",
        "  for m in range(num_models):\n",
        "      models.append(WS_Model(sess, \"model\" + str(m)))    \n",
        "else:\n",
        "  for m in range(num_models):\n",
        "      models.append(ND_Model(sess, \"model\" + str(m)))    \n",
        "\n",
        "sess.run(tf.global_variables_initializer())\n",
        "\n",
        "start_time = time.time() # For checking training time\n",
        "\n",
        "print('Learning Started!')\n",
        "\n",
        "# Train start\n",
        "for epoch in range(training_epochs):\n",
        "         \n",
        "    total_batch = int(bag_size / batch_size)\n",
        "    valid_cost_cv = [0,0,0,0]\n",
        "    \n",
        "    cv = KFold(n_splits=num_cv, shuffle=True)\n",
        "    for train_index, test_index in cv.split(train_data): # Divide the train and test set for cross validation\n",
        "        cv_train_data, cv_validate_data = train_data[train_index], train_data[test_index]\n",
        "        cv_train_target, cv_validate_target = train_target[train_index], train_target[test_index]    \n",
        "                    \n",
        "        for m_idx, m in enumerate(models):\n",
        "            \n",
        "            if m_idx==0: # Train data/label for DTW's CNN model\n",
        "              training, validating = cv_train_data[:,:,0], cv_validate_data[:,:,0]\n",
        "              training_target, validating_target = cv_train_target, cv_validate_target\n",
        "            if m_idx==1: # Train data/label for TP's CNN model\n",
        "              training, validating = cv_train_data[:,:,1], cv_validate_data[:,:,1]\n",
        "              training_target, validating_target = cv_train_target, cv_validate_target\n",
        "            if m_idx==2: # Train data/label for FP's CNN model\n",
        "              training, validating = cv_train_data[:,:,2], cv_validate_data[:,:,2]\n",
        "              training_target, validating_target = cv_train_target, cv_validate_target            \n",
        "            if m_idx==3: # Train data/label for Cubic's CNN model\n",
        "              training, validating = cv_train_data[:,:,3], cv_validate_data[:,:,3]\n",
        "              training_target, validating_target = cv_train_target, cv_validate_target            \n",
        "\n",
        "            training = np.reshape(training,[len(training),n_features,1])\n",
        "            validating = np.reshape(validating,[len(validating),n_features,1])\n",
        "\n",
        "            for i in range(total_batch):\n",
        "\n",
        "                batch_xs, batch_ys = training[batch_size*i:batch_size*(i+1),:], training_target[batch_size*i:batch_size*(i+1)]                \n",
        "                pos_weight = (n_classes - 1) # Weight for weighted cross entropy\n",
        "\n",
        "                _ = m.train(batch_xs, batch_ys, pos_weight, learning_rate) # Train the model with mini-batch data\n",
        "\n",
        "                pos_weight = 1 # No specific weight for weighted cross entropy when calculating validation and test set\n",
        "                        \n",
        "            valid_cost_cv[m_idx] += m.get_cost(validating, validating_target, pos_weight) # Cost for validation set\n",
        "            \n",
        "    valid_cost[epoch] =np.divide(valid_cost_cv ,num_cv) # Average cost for validation set\n",
        "    \n",
        "    if ((epoch+1)%5) == 0: # Print the average cost\n",
        "        print('Epoch:', '%04d' % (epoch + 1), 'Validating_cost_DTW =', round(valid_cost[epoch,0],3),' Validating_cost_TP =', round(valid_cost[epoch,1],3),' Validating_cost_FP =', round(valid_cost[epoch,2],3),' Validating_cost_Cubic =', round(valid_cost[epoch,3],3))         \n",
        "        \n",
        "print('Learning Finished!')\n",
        "\n",
        "end_time = time.time()\n",
        "elapsed = end_time - start_time # For checking training time\n",
        "\n",
        "print('Training time: {:.2f} seconds'.format(elapsed)) # Training time\n",
        "\n"
      ],
      "execution_count": 9,
      "outputs": [
        {
          "output_type": "stream",
          "text": [
            "WARNING:tensorflow:From <ipython-input-7-fa429ad4a794>:23: dropout (from tensorflow.python.layers.core) is deprecated and will be removed in a future version.\n",
            "Instructions for updating:\n",
            "Use keras.layers.dropout instead.\n",
            "WARNING:tensorflow:From /tensorflow-1.15.2/python3.6/tensorflow_core/python/layers/core.py:271: Layer.apply (from tensorflow.python.keras.engine.base_layer) is deprecated and will be removed in a future version.\n",
            "Instructions for updating:\n",
            "Please use `layer.__call__` method instead.\n",
            "WARNING:tensorflow:\n",
            "The TensorFlow contrib module will not be included in TensorFlow 2.0.\n",
            "For more information, please see:\n",
            "  * https://github.com/tensorflow/community/blob/master/rfcs/20180907-contrib-sunset.md\n",
            "  * https://github.com/tensorflow/addons\n",
            "  * https://github.com/tensorflow/io (for I/O related ops)\n",
            "If you depend on functionality not listed there, please file an issue.\n",
            "\n",
            "Learning Started!\n",
            "Epoch: 0005 Validating_cost_DTW = 0.698  Validating_cost_TP = 0.545  Validating_cost_FP = 0.584  Validating_cost_Cubic = 0.535\n",
            "Epoch: 0010 Validating_cost_DTW = 0.509  Validating_cost_TP = 0.386  Validating_cost_FP = 0.419  Validating_cost_Cubic = 0.401\n",
            "Epoch: 0015 Validating_cost_DTW = 0.436  Validating_cost_TP = 0.33  Validating_cost_FP = 0.356  Validating_cost_Cubic = 0.349\n",
            "Epoch: 0020 Validating_cost_DTW = 0.406  Validating_cost_TP = 0.299  Validating_cost_FP = 0.326  Validating_cost_Cubic = 0.323\n",
            "Epoch: 0025 Validating_cost_DTW = 0.386  Validating_cost_TP = 0.28  Validating_cost_FP = 0.308  Validating_cost_Cubic = 0.307\n",
            "Epoch: 0030 Validating_cost_DTW = 0.374  Validating_cost_TP = 0.265  Validating_cost_FP = 0.295  Validating_cost_Cubic = 0.293\n",
            "Epoch: 0035 Validating_cost_DTW = 0.367  Validating_cost_TP = 0.256  Validating_cost_FP = 0.288  Validating_cost_Cubic = 0.285\n",
            "Epoch: 0040 Validating_cost_DTW = 0.362  Validating_cost_TP = 0.253  Validating_cost_FP = 0.281  Validating_cost_Cubic = 0.281\n",
            "Epoch: 0045 Validating_cost_DTW = 0.356  Validating_cost_TP = 0.246  Validating_cost_FP = 0.276  Validating_cost_Cubic = 0.279\n",
            "Epoch: 0050 Validating_cost_DTW = 0.352  Validating_cost_TP = 0.246  Validating_cost_FP = 0.273  Validating_cost_Cubic = 0.272\n",
            "Epoch: 0055 Validating_cost_DTW = 0.351  Validating_cost_TP = 0.239  Validating_cost_FP = 0.269  Validating_cost_Cubic = 0.27\n",
            "Epoch: 0060 Validating_cost_DTW = 0.348  Validating_cost_TP = 0.242  Validating_cost_FP = 0.27  Validating_cost_Cubic = 0.267\n",
            "Learning Finished!\n",
            "Training time: 78.11 seconds\n"
          ],
          "name": "stdout"
        }
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "po5ELhf4tRsO"
      },
      "source": [
        "**Validation Cost Plot**"
      ]
    },
    {
      "cell_type": "code",
      "metadata": {
        "id": "S9R_GjEVtNKj",
        "outputId": "d8849cc2-e29b-4df2-f7e1-29a33ef50787",
        "colab": {
          "base_uri": "https://localhost:8080/",
          "height": 295
        }
      },
      "source": [
        "fig = plt.figure()\n",
        "ax = plt.subplot(111)\n",
        "\n",
        "x_axis = np.linspace(1.0, training_epochs, num=training_epochs)\n",
        "ax.plot(x_axis, valid_cost[:,0],'r', label='DTW')\n",
        "ax.plot(x_axis, valid_cost[:,1],'g', label='TP')\n",
        "ax.plot(x_axis, valid_cost[:,2],'b', label='FP')\n",
        "ax.plot(x_axis, valid_cost[:,3],'m', label='Cubic')\n",
        "\n",
        "ax.legend()\n",
        "plt.title('Validation Cost')\n",
        "plt.xlabel('Epoch')\n",
        "plt.ylabel('Cost')\n",
        "\n",
        "plt.show()"
      ],
      "execution_count": 10,
      "outputs": [
        {
          "output_type": "display_data",
          "data": {
            "image/png": "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\n",
            "text/plain": [
              "<Figure size 432x288 with 1 Axes>"
            ]
          },
          "metadata": {
            "tags": [],
            "needs_background": "light"
          }
        }
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "032-DaVXt0Ae"
      },
      "source": [
        "**Sum Fusion**"
      ]
    },
    {
      "cell_type": "code",
      "metadata": {
        "id": "F9g9mZJCtNPv"
      },
      "source": [
        "testing_0 = np.reshape(test_data[:,:,0], [len(test_data[:,:,0]),n_features,1])\n",
        "testing_1 = np.reshape(test_data[:,:,1], [len(test_data[:,:,1]),n_features,1])\n",
        "testing_2 = np.reshape(test_data[:,:,2], [len(test_data[:,:,2]),n_features,1])\n",
        "testing_3 = np.reshape(test_data[:,:,3], [len(test_data[:,:,3]),n_features,1])\n",
        "\n",
        "start_time_test = time.time() # For checking fusion time\n",
        "\n",
        "test_logit_0 =  models[0].get_predict(testing_0) # Score from DTW's CNN model\n",
        "test_logit_1 =  models[1].get_predict(testing_1) # Score from TP's CNN model\n",
        "test_logit_2 =  models[2].get_predict(testing_2) # Score from FP's CNN model\n",
        "test_logit_3 =  models[3].get_predict(testing_3) # Score from Cubic's CNN model\n"
      ],
      "execution_count": 11,
      "outputs": []
    },
    {
      "cell_type": "code",
      "metadata": {
        "id": "s6paxhjWtNTp",
        "outputId": "c31f4a40-05aa-42bc-b7fb-1281ade6c1d8",
        "colab": {
          "base_uri": "https://localhost:8080/"
        }
      },
      "source": [
        "thres = np.arange(0, 1.001, 0.001) # Threshold for calculating performances\n",
        "\n",
        "thres_matrix_test = np.zeros((len(test_target),len(thres)))\n",
        "total_matrix_test = np.zeros((len(thres),4)) # Saving performances: Accuracy, FAR, FRR, Recall (order of column)\n",
        "\n",
        "test_logit = 0.25*test_logit_0 + 0.25*test_logit_1 + 0.25*test_logit_2 + 0.25*test_logit_3 # Sum fusion with each score\n",
        "\n",
        "\n",
        "for j in range(len(thres)):\n",
        "    for i in range(len(test_target)):\n",
        "        if test_logit[i] < thres[j]:\n",
        "            thres_matrix_test[i,j] = 0 # Below threshold -> Not target\n",
        "        else:\n",
        "            thres_matrix_test[i,j] = 1 # Above (and equal) threshold -> Target\n",
        "\n",
        "\n",
        "# Calculate performances\n",
        "\n",
        "for i in range(len(thres)):\n",
        "\n",
        "    equal_logit = np.equal(thres_matrix_test[:,i], test_target[:,1]) \n",
        "    unique2, True_pos_neg_test = np.unique(thres_matrix_test[np.where(equal_logit==True),i], return_counts=True)\n",
        "    unique3, False_pos_neg_test = np.unique(thres_matrix_test[np.where(equal_logit==False),i], return_counts=True) \n",
        "\n",
        "    if np.shape(True_pos_neg_test)==(1,):\n",
        "        if unique2[0] == 0:\n",
        "            True_pos_neg_test = [True_pos_neg_test[0], 0]\n",
        "        else:\n",
        "            True_pos_neg_test = [0, True_pos_neg_test[0]]\n",
        "\n",
        "    if np.shape(False_pos_neg_test)==(1,):\n",
        "        if unique3[0] == 0:\n",
        "            False_pos_neg_test = [False_pos_neg_test[0], 0]\n",
        "        else:\n",
        "            False_pos_neg_test = [0, False_pos_neg_test[0]]\n",
        "\n",
        "    if np.shape(True_pos_neg_test)==(0,):\n",
        "        True_pos_neg_test = [0, 0]\n",
        "\n",
        "    if np.shape(False_pos_neg_test)==(0,):\n",
        "        False_pos_neg_test = [0, 0]\n",
        "\n",
        "    Recall_test= True_pos_neg_test[1]/(True_pos_neg_test[1]+False_pos_neg_test[0])\n",
        "    Specific_test = True_pos_neg_test[0] /(True_pos_neg_test[0]+False_pos_neg_test[1])\n",
        "\n",
        "    ACC_test = (True_pos_neg_test[0]+True_pos_neg_test[1]) / (len(test_target))\n",
        "    FAR_test = 1 - Specific_test\n",
        "    FRR_test = 1 - Recall_test\n",
        "\n",
        "    total_matrix_test[i,:] = ACC_test, FAR_test, FRR_test, Recall_test\n",
        "\n",
        "fusion_time = time.time() - start_time_test # For checking fusion time\n",
        "elapsed = elapsed + fusion_time\n",
        "\n",
        "print('Final building time: {:.2f} seconds'.format(elapsed))"
      ],
      "execution_count": 12,
      "outputs": [
        {
          "output_type": "stream",
          "text": [
            "Final building time: 79.50 seconds\n"
          ],
          "name": "stdout"
        }
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "UJu4K4qeI0qs"
      },
      "source": [
        "**Result for Target ID**"
      ]
    },
    {
      "cell_type": "code",
      "metadata": {
        "id": "PRVTsER4tNOJ",
        "outputId": "b6f49fa8-4b34-467f-f360-76efff9a8faa",
        "colab": {
          "base_uri": "https://localhost:8080/",
          "height": 329
        }
      },
      "source": [
        "EER_line = thres[::-1]\n",
        "\n",
        "fig2 = plt.figure()\n",
        "ax2 = plt.subplot(111)\n",
        "ax2.plot(total_matrix_test[:,1], total_matrix_test[:,3],'r',label='ROC')\n",
        "ax2.plot(thres, EER_line, 'b',label='EER Line')\n",
        "ax2.legend()\n",
        "plt.title('ROC Curve')\n",
        "plt.xlabel('FAR')\n",
        "plt.ylabel('TPR')\n",
        "plt.show()\n",
        "\n",
        "EER_loc_test = np.argmin(abs(total_matrix_test[:,1] - total_matrix_test[:,2]))\n",
        "print('Accuracy: {:.2f}% '.format(100*total_matrix_test[EER_loc_test,0]))\n",
        "print('EER: {:.2f}% '.format(100*(total_matrix_test[EER_loc_test,1]+total_matrix_test[EER_loc_test,2])/2))\n",
        "\n"
      ],
      "execution_count": 13,
      "outputs": [
        {
          "output_type": "display_data",
          "data": {
            "image/png": "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\n",
            "text/plain": [
              "<Figure size 432x288 with 1 Axes>"
            ]
          },
          "metadata": {
            "tags": [],
            "needs_background": "light"
          }
        },
        {
          "output_type": "stream",
          "text": [
            "Accuracy: 96.04% \n",
            "EER: 4.06% \n"
          ],
          "name": "stdout"
        }
      ]
    }
  ]
}